package ace.cmp.spring.data.jdbc.querydsl.core.repository;

import ace.cmp.core.exception.BusinessI18nException;
import ace.cmp.spring.data.jdbc.querydsl.core.QuerydslJdbcRepositoryExtendFactoryInjectHelper;
import ace.cmp.spring.data.jdbc.querydsl.core.QuerydslRelationHelper;
import ace.cmp.spring.data.jdbc.querydsl.core.dml.QuerydslMapper;
import ace.cmp.spring.data.jdbc.querydsl.core.repository.handler.QuerydslCreatedDateAnnotationHandler;
import ace.cmp.spring.data.jdbc.querydsl.core.repository.handler.QuerydslLastModifiedDateAnnotationHandler;
import ace.cmp.spring.data.jdbc.querydsl.core.repository.handler.QuerydslLogicDeleteAnnotationHandler;
import ace.cmp.spring.data.jdbc.querydsl.core.repository.handler.QuerydslVersionAnnotationHandler;
import ace.cmp.spring.data.jdbc.querydsl.core.util.PageUtils;
import ace.cmp.spring.data.jdbc.querydsl.core.util.QuerydslAnnotationUtils;
import com.infobip.spring.data.jdbc.QuerydslJdbcPredicateExecutor;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.SimpleExpression;
import com.querydsl.sql.RelationalPath;
import com.querydsl.sql.RelationalPathBase;
import com.querydsl.sql.SQLQuery;
import com.querydsl.sql.SQLQueryFactory;
import com.querydsl.sql.dml.DefaultMapper;
import com.querydsl.sql.dml.Mapper;
import com.querydsl.sql.dml.SQLInsertClause;
import com.querydsl.sql.dml.SQLUpdateClause;
import jakarta.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.SneakyThrows;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

/**
 * @author caspar
 * @date 2023/4/3 13:58
 */
@Transactional(readOnly = true)
public class QuerydslJdbcBaseFragmentImpl<T, Id> implements QuerydslJdbcBaseFragment<T, Id> {

  private final SQLQueryFactory sqlQueryFactory;
  private final RelationalPath<T> entityPath;
  private final RelationalPersistentEntity<T> entityPersistentPath;
  private final QuerydslJdbcPredicateExecutor<T> predicateExecutor;
  private final QuerydslRelationHelper querydslRelationHelper;
  private final Path primaryKeyPath;
  private final Path logicDeletePath;
  private final SimpleExpression primaryKeyExpression;
  private final SimpleExpression logicDeleteKeyExpression;
  private final Class entityType;
  private final Class idType;
  private final QuerydslCreatedDateAnnotationHandler querydslCreatedDateAnnotationHandler;
  private final QuerydslLastModifiedDateAnnotationHandler querydslLastModifiedDateAnnotationHandler;
  private final QuerydslVersionAnnotationHandler querydslVersionAnnotationHandler;
  private final QuerydslLogicDeleteAnnotationHandler querydslLogicDeleteAnnotationHandler;
  private final QuerydslAnnotationUtils querydslAnnotationUtils;

  public QuerydslJdbcBaseFragmentImpl(
      Class entityType,
      Class idType,
      SQLQueryFactory sqlQueryFactory,
      QuerydslJdbcPredicateExecutor<T> predicateExecutor,
      QuerydslJdbcRepositoryExtendFactoryInjectHelper
          querydslJdbcRepositoryExtendFactoryInjectHelper) {
    this.entityType = entityType;
    this.idType = idType;
    this.querydslRelationHelper =
        querydslJdbcRepositoryExtendFactoryInjectHelper.getQuerydslRelationHelper();
    this.sqlQueryFactory = sqlQueryFactory;
    this.predicateExecutor = predicateExecutor;
    this.entityPath =
        this.querydslRelationHelper.getRelationalPathBaseByEntityType(this.entityType);
    this.entityPersistentPath =
        this.querydslRelationHelper.getRelationalPersistentEntityByEntityType(this.entityType);
    this.primaryKeyExpression =
        this.querydslRelationHelper.getPrimaryKeyExpression(this.entityType);
    this.primaryKeyPath = this.querydslRelationHelper.getPrimaryKeyPath(entityType);
    this.logicDeleteKeyExpression =
        this.querydslRelationHelper.getLogicDeleteExpression(this.entityType);
    this.logicDeletePath = this.querydslRelationHelper.getLogicDeletePath(this.entityType);
    this.querydslCreatedDateAnnotationHandler =
        querydslJdbcRepositoryExtendFactoryInjectHelper.getQuerydslCreatedDateAnnotationHandler();
    this.querydslLastModifiedDateAnnotationHandler =
        querydslJdbcRepositoryExtendFactoryInjectHelper
            .getQuerydslLastModifiedDateAnnotationHandler();
    this.querydslVersionAnnotationHandler =
        querydslJdbcRepositoryExtendFactoryInjectHelper.getQuerydslVersionAnnotationHandler();
    this.querydslLogicDeleteAnnotationHandler =
        querydslJdbcRepositoryExtendFactoryInjectHelper.getQuerydslLogicDeleteAnnotationHandler();
    this.querydslAnnotationUtils =
        querydslJdbcRepositoryExtendFactoryInjectHelper.getQuerydslAnnotationUtils();
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long insert(T entity) {
    return this.insert(entity, true);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long insert(T entity, boolean saveNullFieldFlag) {
    return this.insert(entity, saveNullFieldFlag, false, false, false, false);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long insert(
      T entity,
      boolean saveNullFieldFlag,
      boolean enableVersionForce,
      boolean enableCreatedDateForce,
      boolean enableLastModifiedDateForce,
      boolean enableLogicDeleteForce) {
    return this.insert(
        Collections.singletonList(entity),
        saveNullFieldFlag,
        enableVersionForce,
        enableCreatedDateForce,
        enableLastModifiedDateForce,
        enableLogicDeleteForce);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long insert(Collection<T> entities) {
    return this.insert(entities, true);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long insert(Collection<T> entities, boolean saveNullFieldFlag) {
    return this.insert(entities, saveNullFieldFlag, false, false, false, false);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long insertNotNullFieldAndInjectFrameworkField(Collection<T> entities) {
    return this.insert(entities, false, true, true, true, true);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long insertNotNullFieldAndInjectFrameworkField(T entity) {
    return this.insert(entity, false, true, true, true, true);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long insert(
      Collection<T> entities,
      boolean saveNullFieldFlag,
      boolean enableVersionForce,
      boolean enableCreatedDateForce,
      boolean enableLastModifiedDateForce,
      boolean enableLogicDeleteForce) {
    return this.insert0(
        entities,
        saveNullFieldFlag,
        enableVersionForce,
        enableCreatedDateForce,
        enableLastModifiedDateForce,
        enableLogicDeleteForce);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long updateById(T entity) {
    return this.updateById(entity, true);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long updateById(T entity, boolean saveNullFieldFlag) {
    return this.updateById(entity, saveNullFieldFlag, false, false, false, false);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long updateById(
      T entity,
      boolean saveNullFieldFlag,
      boolean enableVersionOnlyUpdate,
      boolean enableCreatedDateForce,
      boolean enableLastModifiedDateForce,
      boolean enableLogicDeleteForce) {
    return this.updateById(
        Collections.singletonList(entity),
        saveNullFieldFlag,
        enableVersionOnlyUpdate,
        enableCreatedDateForce,
        enableLastModifiedDateForce,
        enableLogicDeleteForce);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long updateById(Collection<T> entities) {
    return this.updateById(entities, true);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long updateById(Collection<T> entities, boolean saveNullFieldFlag) {
    return this.updateById(entities, saveNullFieldFlag, false, false, false, false);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long updateById(
      Collection<T> entities,
      boolean saveNullFieldFlag,
      boolean enableVersionOnlyUpdate,
      boolean enableCreatedDateForce,
      boolean enableLastModifiedDateForce,
      boolean enableLogicDeleteForce) {
    return this.updateById0(
        entities,
        saveNullFieldFlag,
        enableVersionOnlyUpdate,
        enableCreatedDateForce,
        enableLastModifiedDateForce,
        enableLogicDeleteForce);
  }

  protected Mapper<Object> getMapperBy(
      boolean saveNullFieldFlag, boolean enableIgnoreAnnotationMapper) {
    Mapper<Object> parentMapper =
        saveNullFieldFlag ? DefaultMapper.WITH_NULL_BINDINGS : DefaultMapper.DEFAULT;
    if (!enableIgnoreAnnotationMapper) {
      return parentMapper;
    }
    Set<String> ignoreFieldNames =
        this.querydslAnnotationUtils.getAllFieldByFrameworkAnnotationMap(entityType).keySet();
    return new QuerydslMapper(parentMapper, ignoreFieldNames);
  }

  protected long insert0(
      Collection<T> entities,
      boolean saveNullFieldFlag,
      boolean enableVersionForce,
      boolean enableCreatedDateForce,
      boolean enableLastModifiedDateForce,
      boolean enableLogicDeleteForce) {
    SQLInsertClause sqlInsertClause = this.sqlQueryFactory.insert(entityPath);

    Mapper<Object> mapper = this.getMapperBy(saveNullFieldFlag, true);

    entities.forEach(
        entity -> {
          sqlInsertClause.populate(entity, mapper);

          this.querydslVersionAnnotationHandler.processInsert(
              entity, sqlInsertClause, enableVersionForce);

          this.querydslCreatedDateAnnotationHandler.processInsert(
              entity, sqlInsertClause, enableCreatedDateForce);

          this.querydslLastModifiedDateAnnotationHandler.processInsert(
              entity, sqlInsertClause, enableLastModifiedDateForce);

          this.querydslLogicDeleteAnnotationHandler.processInsert(
              entity, sqlInsertClause, enableLogicDeleteForce);

          sqlInsertClause.addBatch();
        });

    return sqlInsertClause.execute();
  }

  /**
   * 修改数据
   *
   * @param entities                    实体
   * @param saveNullFieldFlag           是否保存null字段
   * @param enableVersionOnlyUpdate     启用版本号注解,仅自动更新版本号,不进行版本号过滤
   * @param enableCreatedDateForce      启用创建时间注解,忽略用户提交的值
   * @param enableLastModifiedDateForce 启用最后修改时间注解,忽略用户提交的值
   * @param enableLogicDeleteForce      启用逻辑删除注解,忽略用户提交的值
   * @return 影响行数
   */
  protected long updateById0(
      Collection<T> entities,
      boolean saveNullFieldFlag,
      boolean enableVersionOnlyUpdate,
      boolean enableCreatedDateForce,
      boolean enableLastModifiedDateForce,
      boolean enableLogicDeleteForce) {
    if (this.entityPath.getPrimaryKey() == null
        || this.entityPath.getPrimaryKey().getLocalColumns().isEmpty()) {
      throw new RuntimeException("id can't be null");
    }

    if (this.entityPath.getPrimaryKey().getLocalColumns().size() > 1) {
      throw new RuntimeException("multiple id item is not support ");
    }

    Mapper<Object> mapper = this.getMapperBy(saveNullFieldFlag, true);

    SQLUpdateClause sqlUpdateClause = this.sqlQueryFactory.update(entityPath);

    for (T entity : entities) {
      Predicate idPredicate = this.getPrimaryKeyPredicate(entity);

      Assert.notNull(idPredicate, "get id expression failure");

      sqlUpdateClause.populate(entity, mapper);

      this.querydslVersionAnnotationHandler.processUpdate(
          entity, sqlUpdateClause, enableVersionOnlyUpdate);

      this.querydslCreatedDateAnnotationHandler.processUpdate(
          entity, sqlUpdateClause, enableCreatedDateForce);

      this.querydslLastModifiedDateAnnotationHandler.processUpdate(
          entity, sqlUpdateClause, enableLastModifiedDateForce);

      this.querydslLogicDeleteAnnotationHandler.processUpdate(
          entity, sqlUpdateClause, enableLogicDeleteForce);

      sqlUpdateClause.where(idPredicate);

      sqlUpdateClause.addBatch();
    }

    long updateRowCount = sqlUpdateClause.execute();

    if (updateRowCount != (long) entities.size()) {
      throw new OptimisticLockingFailureException("optimistic locking failure");
    }

    return updateRowCount;
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long deleteLogicById(Collection<Id> ids) {
    long resultRow = 0L;
    if (CollectionUtils.isEmpty(ids)) {
      return resultRow;
    }
    if (!this.hasLogicDeleteProperty()) {
      throw new RuntimeException("cannot find the id property." + this.entityType.getName());
    }

    SQLUpdateClause sqlUpdateClause = this.sqlQueryFactory.update(this.entityPath);

    for (Id id : ids) {
      this.querydslLogicDeleteAnnotationHandler.processLogicDelete(this.primaryKeyExpression, this.entityType, sqlUpdateClause);
      sqlUpdateClause.where(this.primaryKeyExpression.eq(id));
      sqlUpdateClause.addBatch();
    }
    resultRow = sqlUpdateClause.execute();
    return resultRow;
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long deleteLogicBy(Predicate predicate) {
    Assert.notNull(predicate, "predicate must not be null");

    long resultRow = 0L;

    if (!this.hasLogicDeleteProperty()) {
      throw new RuntimeException("cannot find the id property." + this.entityType.getName());
    }

    SQLUpdateClause sqlUpdateClause = this.sqlQueryFactory.update(this.entityPath);

    this.querydslLogicDeleteAnnotationHandler.processLogicDelete(
        this.primaryKeyExpression, this.entityType, sqlUpdateClause);

    sqlUpdateClause.where(predicate);

    resultRow = sqlUpdateClause.execute();
    return resultRow;
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long deleteLogicById(Id... ids) {
    return this.deleteLogicById(Arrays.stream(ids).toList());
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long deleteLogicByIdForEntity(Collection<T> entities) {
    List<Id> ids = entities.stream().map(this::getEntityId).collect(Collectors.toList());
    return this.deleteLogicById(ids);
  }

  @Transactional(rollbackFor = Exception.class)
  @Override
  public long deleteLogicByIdForEntity(T entity) {
    return this.deleteLogicById(this.getEntityId(entity));
  }

  @SneakyThrows
  @Override
  public Map<Id, T> findAllMapByIds(Collection<Id> ids) {
    if (ids == null || ids.isEmpty()) {
      return MapUtils.EMPTY_MAP;
    }

    Predicate predicate = this.primaryKeyExpression.in(ids);

    List<T> results = this.predicateExecutor.findAll(predicate);

    return this.toMap(results);
  }

  @Override
  public Map<Id, T> findAllMapBy(Predicate predicate) {
    Assert.notNull(predicate, "Predicate must not be null!");
    return this.findAllMapBy(predicate, Sort.unsorted());
  }

  @Override
  public Map<Id, T> findAllMapBy(Predicate predicate, Sort sort) {
    Assert.notNull(predicate, "Predicate must not be null!");
    Assert.notNull(sort, "Sort must not be null!");
    List<T> results = this.predicateExecutor.findAll(predicate, sort);
    return this.toMap(results);
  }

  @Override
  public Map<Id, T> findAllMapBy(Predicate predicate, OrderSpecifier<?>... orders) {
    Assert.notNull(predicate, "Predicate must not be null!");
    Assert.notNull(orders, "Order specifiers must not be null!");
    List<T> results = this.predicateExecutor.findAll(predicate, orders);
    return this.toMap(results);
  }

  @Override
  public T findByIdAndDeleteFlagIsNot(Id id) {
    if (this.hasLogicDeleteProperty()) {
      List<Predicate> predicates = new ArrayList<>();
      Predicate idPredicate = this.primaryKeyExpression.eq(id);
      predicates.add(idPredicate);

      Object notDeleteFlagValue =
          this.querydslRelationHelper.getLogicNotDeleteFlagValue(this.entityType);
      Predicate deleteFlagPredicate = this.logicDeleteKeyExpression.eq(notDeleteFlagValue);
      predicates.add(deleteFlagPredicate);
      Predicate predicate = ExpressionUtils.allOf(predicates);
      T results = this.predicateExecutor.findOne(predicate).orElse(null);

      return results;
    } else {
      return this.getById(id);
    }
  }

  @Override
  public List<T> findByIdInAndDeleteFlagIsNot(Collection<Id> ids) {
    if (this.hasLogicDeleteProperty()) {
      List<Predicate> predicates = new ArrayList<>();
      Predicate idPredicate = this.primaryKeyExpression.in(ids);
      predicates.add(idPredicate);

      Object notDeleteFlagValue = this.querydslRelationHelper.getLogicNotDeleteFlagValue(this.entityType);
      Predicate deleteFlagPredicate = this.logicDeleteKeyExpression.eq(notDeleteFlagValue);
      predicates.add(deleteFlagPredicate);
      Predicate predicate = ExpressionUtils.allOf(predicates);
      List<T> results = this.predicateExecutor.findAll(predicate);

      return results;
    } else {
      return this.getByIdIn(ids);
    }
  }

  @Override
  public T findAndCheckDataIsNotNullByIdAndDeleteFlagIsNot(Id id) {
    T entity = this.findByIdAndDeleteFlagIsNot(id);
    if (entity == null) {
      throw new BusinessI18nException(this.getEntityClass().getName() + ".not-found", "数据不存在");
    }
    return entity;
  }

  @Nonnull
  @Override
  public T findAndCheckDataIsNotNullById(Id id) {
    T entity = this.getById(id);
    if (entity == null) {
      throw new BusinessI18nException(this.getEntityClass().getName() + ".not-found", "数据不存在");
    }
    return entity;
  }

  @Override
  public Page<T> page(Predicate predicate, Pageable pageable) {
    return PageUtils.page(this.getEntitySqlQuery(), predicate, pageable);
  }

  @Override
  public Page<T> page(RelationalPathBase<T> entityPath, Predicate predicate, Pageable pageable) {
    return PageUtils.page(this.getEntitySqlQuery(entityPath), predicate, pageable);
  }

  @Override
  public Page<T> page(Collection<Predicate> predicates, Pageable pageable) {
    return PageUtils.page(this.getEntitySqlQuery(), ExpressionUtils.allOf(predicates), pageable);
  }

  @Override
  public Page<T> page(RelationalPathBase<T> entityPath, Collection<Predicate> predicates,
      Pageable pageable) {
    return PageUtils.page(this.getEntitySqlQuery(entityPath), ExpressionUtils.allOf(predicates),
        pageable);
  }

  @Override
  public Page<T> page(Pageable pageable) {
    return PageUtils.page(this.getEntitySqlQuery(), pageable);
  }

  @Override
  public long countByIdIn(List<Id> ids) {
    Predicate predicate = this.primaryKeyExpression.in(ids);
    return this.getEntitySqlQuery()
        .where(predicate)
        .fetchCount();
  }

  @Override
  public Class<T> getEntityClass() {
    return this.entityType;
  }

  @Override
  public Class<Id> getIdClass() {
    return this.idType;
  }

  private SQLQuery<T> getEntitySqlQuery() {
    return this.sqlQueryFactory.query()
        .select(this.entityPath)
        .from(this.entityPath);
  }

  private SQLQuery<T> getEntitySqlQuery(RelationalPathBase<T> path) {
    return this.sqlQueryFactory.query()
        .select(path)
        .from(path);
  }

  @SneakyThrows
  private Predicate getPrimaryKeyPredicate(T entity) {
    if (this.entityPath.getPrimaryKey() == null
        || this.entityPath.getPrimaryKey().getLocalColumns().isEmpty()) {
      return null;
    }
    Predicate idPredicate = null;

    for (Path propertyPath : entityPath.getPrimaryKey().getLocalColumns()) {
      String propertyName = propertyPath.getMetadata().getName();
      Object id = PropertyUtils.getProperty(entity, propertyName);
      SimpleExpression expression = (SimpleExpression) propertyPath;
      idPredicate = ExpressionUtils.and(idPredicate, expression.eq(id));
    }

    return idPredicate;
  }

  /**
   * 是否存在逻辑删除属性
   *
   * @return
   */
  private boolean hasLogicDeleteProperty() {
    return this.logicDeleteKeyExpression != null;
  }

  @Override
  public T getById(Id id) {
    Predicate predicate = this.primaryKeyExpression.eq(id);

    T results = this.predicateExecutor.findOne(predicate).orElse(null);

    return results;
  }

  @Override
  public List<T> getByIdIn(Collection<Id> id) {
    Predicate predicate = this.primaryKeyExpression.in(id);

    List<T> results = this.predicateExecutor.findAll(predicate);

    return results;
  }

  @SneakyThrows
  private Map<Id, T> toMap(List<T> entities) {
    Map<Id, T> resultMap = new HashMap<>(entities.size());

    for (T result : entities) {
      String firstIdProperty = this.entityPersistentPath.getRequiredIdProperty().getName();
      Id id = (Id) PropertyUtils.getProperty(result, firstIdProperty);
      resultMap.put(id, result);
    }

    return resultMap;
  }

  @SneakyThrows
  private Id getEntityId(T entity) {
    return querydslRelationHelper.getEntityId(entity);
  }


}
